<?php
#
# dmBridge: a data access framework for CONTENTdm(R)
#
# Copyright © 2009, 2010, 2011 Board of Regents of the Nevada System of Higher
# Education, on behalf of the University of Nevada, Las Vegas
#

/**
 * <p>Subclass of DMURI that represents intra-dmBridge URIs.</p>
 *
 * <p>This class enables accessing the dmBridge "params," which are a subset
 * of the "path" portion of the URI beginning at the base URI and ending at the
 * path extension. Given the following URI as an example:</p>
 *
 * <p>http://www.bla.com/dm/objects/bla/59.atom</p>
 *
 * <p>The dmBridge URI namespace begins beneath "/dm/", and the "params,"
 * therefore, are "objects/bla/59."</p>
 *
 * @author Alex Dolski <alex.dolski@unlv.edu>
 * @license http://www.opensource.org/licenses/mit-license.php
 */
class DMInternalURI extends DMURI {
	
	/**
	 * @var DOMDocument The DOMDocument representation of routes.xml. Access
	 * via getRoutesXML().
	 */
	private static $routes_xml;

	/**
	 * @var string
	 */
	private $extension;

	/**
	 * @return array
	 */
	private static function getAllowedExtensions() {
		return array_merge(DMMediaType::getKnownExtensions(),
				array("xml", "json", "jsonp", "atom", "html", "sitemap"));
	}

	/**
	 * @param string path
	 * @return string
	 */
	private static function getPathMinusExtension($path) {
		$tmp = explode(".", $path);
		if (count($tmp) > 1) {
			$ext = $tmp[count($tmp) - 1];
			if (in_array($ext, DMMediaType::getKnownExtensions())) {
				array_pop($tmp);
				return implode(".", $tmp);
			}
		}
		return $path;
	}

	/**
	 * Returns the RESTful URI of a dmBridge model object.
	 *
	 * @param int component The component for which the URI should be
	 * generated (one of the constants in the DMBridgeComponent class)
	 * @param string params The URI params
	 * @param string representation Optional alternate representation such as
	 * "atom"; default is HTML
	 * @return DMInternalURI
	 */
	public static function getResourceURI($component,
			$params, $representation = null) {
		$prefix = "";
		switch ($component) {
		case DMBridgeComponent::HTTPAPI:
			$prefix = "api/1/";
			break;
		default:
			break;
		}

		return self::getURIWithParams($prefix . $params, array(),
				$representation == "html" ? null : $representation);
	}

	/**
	 * @param string params
	 * @param string path Absolute path of the XML route file to search; supply
	 * null for default
	 * @return DMRoute
	 */
	private static function getRouteMatchingParamsFromRoutesFile(
			$params, $path = null) {
		// parse routes XML
		if ($path == null) {
			$routes_xml = self::getRoutesXML();
		} else {
			if (file_exists($path)) {
				$routes_xml = new DOMDocument("1.0", "utf-8");
				$routes_xml->load($path);
			} else {
				return null;
			}
		}

		// parse the URI
		$uri_parts = explode("/", $params);
		$uri_count = count($uri_parts);

		$version = 1; // default resource version; may be overridden below
		$xml_route_parts = array();
		$props = array(
			'ctrlr' => null,
			'method' => null,
			'parameters' => null
		);
		$representations = array();
		foreach ($routes_xml->getElementsByTagName("route") as $xml_route) {
			$xml_route_parts = explode("/", $xml_route->getAttribute("path"));
			$xml_route_count = count($xml_route_parts);
			if ($uri_count != $xml_route_count) { // not a match
				continue;
			}
			$parameters = array();
			for ($i = 0; $i < $uri_count; $i++) {
				if ($xml_route_parts[$i] != $uri_parts[$i]
						&& strpos($xml_route_parts[$i], ":") !== 0) {
					break; // not a match
				}
				if (strpos($xml_route_parts[$i], ":") === 0) {
					$parameters[] = $uri_parts[$i];
					if ($xml_route_parts[$i] == ":int") {
						if ((string) $uri_parts[$i] != (string) round($uri_parts[$i])) {
							break; // not a match
						}
					}
				}

				if ($i == $uri_count - 1) { // successful match
					$invocation = $xml_route
						->getElementsByTagName("invocation")->item(0);
					$props['ctrlr'] = $invocation
						->getElementsByTagName("class")->item(0)->nodeValue;
					$props['method'] = $invocation
						->getElementsByTagName("method")->item(0)->nodeValue;
					$props['parameters'] = implode(",", $parameters);
					$xml_representations = $xml_route
						->getElementsByTagName("representations")->item(0);
					foreach ($xml_representations->getElementsByTagName(
							"representation") as $rep) {
						$representations[$rep->getAttribute("ext")]
								= DMMediaType::getTypeForString($rep->nodeValue);
					}
					$version = (int) $xml_route
						->getElementsByTagName("resourceVersion")->item(0)
							->nodeValue;
					break(2);
				}
			}
		}

		if ($props['ctrlr'] && $props['method']) {
			$route = new DMRoute($props['ctrlr'], $props['method']);
			if ($props['parameters']) {
				$route->setParameters($props['parameters']);
			}
			$route->setAvailableRepresentations($representations);

			return $route;
		}
		return null;
	}

	/**
	 * @return DOMDocument
	 */
	private static function getRoutesXML() {
		if (!self::$routes_xml) {
			$path = dirname(__FILE__) . "/../../includes/routes.xml";
			self::$routes_xml = new DOMDocument("1.0", "utf-8");
			self::$routes_xml->load($path);
		}
		return self::$routes_xml;
	}

	/**
	 * @param string params
	 * @param array query Array of query string key-value pairs in the format
	 * accepted by http_build_query()
	 * @param string representation An optional representation extension to
	 * append to the URL (e.g. "atom"). Pass null to use the current
	 * representation, or false to not explicitly specify a representation in
	 * the URI.
	 * @return DMInternalURI
	 */
	public static function getURIWithParams($params, array $query = array(),
			$representation = null) {
		$path = DMConfigIni::getInstance()->getString("dmbridge.base_uri_path");
		$uri = clone DMHTTPRequest::getCurrent()->getURI();
		$uri->unsetQuery();
		$uri->setPath($path);
		$uri->setParams($params);
		$uri->setExtension($representation);
		
		if (array_key_exists("r", $query) && !$params) {
			$uri->setParams($query['r']);
		}

		foreach ($query as $k => $v) {
			$uri->addQueryValue($k, $v);
		}
		return $uri;
	}

	/**
	 * @return string The string representation of the URI, or null if the URI
	 * is invalid.
	 * @Override
	 */
	public function getAbsoluteURIAsString() {
		if (!$this->getScheme() || !$this->getHost()) {
			return null;
		}

		$clone = clone $this;
		$uri = $clone->getScheme() . "://";
		if ($clone->getUser()) {
			$uri .= $clone->getUser();
		}
		if ($clone->getPassword()) {
			$uri .= ":" . $clone->getPassword();
		}
		if ($clone->getUser() || $clone->getPassword()) {
			$uri .= "@";
		}
		$uri .= $clone->getHost();
		if ($clone->getPort() != 80) {
			$uri .= ":" . $clone->getPort();
		}
		$uri .= $clone->getPath();
		if ($clone->getExtension() && !($clone->isTemplateEngine()
				&& $clone->getExtension() == "html")) {
			$uri .= "." . $clone->getExtension();
		}
		$clone->unsetQueryKey("r");
		if (count($clone->getQuery())) {
			$uri .= "?" . $clone->getQueryString();
		}
		if ($clone->getFragment()) {
			$uri .= "#" . $clone->getFragment();
		}
		return $uri;
	}

	/**
	 * @return boolean True if the URI points to the Control Panel.
	 */
	public function isControlPanel() {
		$components = $this->getParamComponents();
		return (bool) count($components) && $components[0] == "admin";
	}

	/**
	 * @return string
	 */
	public function getExtension() {
		return $this->extension;
	}

	/**
	 * @param string ext
	 */
	public function setExtension($ext) {
		$this->extension = $ext;
	}

	/**
	 * @return boolean True if the URI points to the HTTP API.
	 */
	public function isHTTPAPI() {
		$components = $this->getParamComponents();
		return (bool) count($components) && $components[0] == "api";
	}

	/**
	 * @return DMRoute
	 */
	private function getModuleRoute() {
		$mm = DMModuleManager::getInstance();
		foreach ($mm->getEnabledModules() as $module) {
			$route = self::getRouteMatchingParamsFromRoutesFile(
					$this->getParams(),
					$module->getAbsolutePathname() . "/routes.xml");
			if ($route) {
				$route->setModuleRoute(true);
				return $route;
			}
		}
		return null;
	}

	/**
	 * @return string The "params" portion of the URI; e.g. "api/1/collections"
	 * or "objects/alias/123".
	 */
	public function getParams() {
		$path = DMConfigIni::getInstance()->getString("dmbridge.base_uri_path");
		return trim(str_replace($path, "",
				self::getPathMinusExtension($this->getPath())), "/");
	}

	/**
	 * @param string params
	 */
	public function setParams($params) {
		$path = DMConfigIni::getInstance()->getString("dmbridge.base_uri_path");
		$this->setPath($path . "/" . $params);
	}

	/**
	 * @return array The slash-delineated components of the params, as an
	 * array.
	 */
	public function getParamComponents() {
		return explode("/", $this->getParams());
	}

	/**
	 * @param DMInternalURI uri
	 * @return DMRoute
	 */
	public function getRoute() {
		$r = $this->getModuleRoute();
		if ($r) {
			return $r;
		}

		$tmp1 = $this->getParamComponents();

		$dxp = new DOMXPath(self::getRoutesXML());
		$result = $dxp->query("//route/@path");

		// look for built-in routes
		foreach ($result as $route) {
			$tmp2 = explode("/", $route->value);

			$count1 = count($tmp1);
			$count2 = count($tmp2);
			if ($count1 == $count2) {
				for ($i = 0; $i < $count1; $i++) {
					if ($i + 1 == $count1) { // match
						$r = self::getRouteMatchingParamsFromRoutesFile(
								$this->getParams());
						if ($r) {
							return $r;
						}
					}
				}
			}
		}
	}

	/**
	 * @Override
	 * @param string str
	 * @throws DMIllegalArgumentException if $str is not a valid URI
	 */
	public function setString($str) {
		$tmp = parse_url($str);

		if (!is_array($tmp)) {
			throw new DMIllegalArgumentException(
					DMLocalizedString::getString("INVALID_URL"));
		}

		if (array_key_exists("scheme", $tmp)) {
			$this->setScheme($tmp['scheme']);
		}
		if (array_key_exists("host", $tmp)) {
			$this->setHost($tmp['host']);
		}
		if (array_key_exists("port", $tmp)) {
			$this->setPort($tmp['port']);
		}
		if (array_key_exists("user", $tmp)) {
			$this->setUser($tmp['user']);
		}
		if (array_key_exists("pass", $tmp)) {
			$this->setPassword($tmp['pass']);
		}
		if (array_key_exists("path", $tmp)) {
			$tmp2 = explode(".", $tmp['path']);
			$last_part = $tmp2[count($tmp2)-1];
			if (in_array($last_part, self::getAllowedExtensions())) {
				$this->setExtension(array_pop($tmp2));
				$path = implode(".", $tmp2);
				$this->setPath($path);
			} else {
				$this->setPath($tmp['path']);
			}
		}
		$this->unsetQuery();
		if (array_key_exists("query", $tmp)) {
			$q = explode("&", $tmp['query']);
			foreach ($q as $kv) {
				$parts = explode("=", $kv);
				if (count($parts) == 2) {
					$this->addQueryValue(
							urldecode($parts[0]), urldecode($parts[1]));
				}
			}
		}
		if (array_key_exists("fragment", $tmp)) {
			$this->setFragment($tmp['fragment']);
		}
	}

	/**
	 * @return boolean True if the URI points to the template engine.
	 */
	public function isTemplateEngine() {
		$components = $this->getParamComponents();
		return (bool) count($components) && $components[0] == "objects";
	}

	/**
	 * @return int The version of the resource specified by the URI params,
	 * typically the second part of the params; e.g. "api/1/collections". Not
	 * all URIs will have a version, in which case this method will return
	 * null.
	 */
	public function getVersion() {
		$parts = $this->getParamComponents();
		if (count($parts) > 1 && is_numeric($parts[1])) {
			return $parts[1];
		}
		return null;
	}

}
